/* @flow */

import { warn } from '../util/index'
import { hasOwn } from 'shared/util'
import { hasSymbol } from 'core/util/env'
import { defineReactive, observerState } from '../observer/index'

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      //依赖注入中的提供者可以设置为一个函数，该函数的执行上下文为当前vm实例，并且该函数可以访问data、props、computed等
      //所有vm上的属性。
      ? provide.call(vm)
      : provide
  }
}

export function initInjections (vm: Component) {
  // 调用resolveInject方法在当前vm的祖先实例中查找inject中对应的每个
  // 需注入的字段。该方法返回的值为以inject中的键为键，以provider中对应的
  // 值为值。
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    //observerState.shouldConvert为false，则设置数据响应系统时，
    //如果值为对象，也不会为其再添加响应监听，而如果为true，并且值为对象，则会依次为
    //当前值对象及其子对象设置监听响应。也就是说，即使依赖注入中的注入方的对象改变其内部数据，
    //也不会引起被注入方的监听，因为根本就没有监听
    observerState.shouldConvert = false
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    observerState.shouldConvert = true
  }
}

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    const keys = hasSymbol
        ? Reflect.ownKeys(inject)
        : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      const provideKey = inject[key]
      let source = vm
      //虽然是从当前vue实例开始遍历，但是因为当前vue实例的provider还没有解析，
      //所以无法从当前vue实例中获得注入。
      while (source) {
        //_provided属性是在initProvide方法中添加的。
        if (source._provided && provideKey in source._provided) {
          //inject属性最终的修正后的形式为：{xxx1:'xxx2'}，
          //可将xxx1为在当前Vue实例中使用的名字，而xxx2是在provider中查找相应
          //信息使用的名字
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (process.env.NODE_ENV !== 'production' && !hasOwn(result, key)) {
        warn(`Injection "${key}" not found`, vm)
      }
    }
    return result
  }
}
